package routers

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"strconv"
	"sync"

	"gitee.com/law-api/log"
)

//ChargeRequest receive charge-data request.
type ChargeRequest struct {
	URL  string               `json:"url"`
	Data []*ChargeRequestData `json:"data"`
}

//ChargeReqrlestData fill a account list and its amount should increase
type ChargeRequestData struct {
	Addr   string `json:"chain_address"`
	Amount string `json:"charge_amount"`
}

//ChargeResponse return the status about charge request
type ChargeResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

func newChargeResponse(err error) *ChargeResponse {
	code := checkChargeErrCode(err)
	//if code isn't 0, return its error message
	msg := "成功"
	if code != 0 {
		msg = err.Error()
	}
	return &ChargeResponse{
		Code: code,
		Msg:  msg,
	}
}

//ChargeStatusResponse return the status should be posted to callback url
type ChargeStatusResponse struct {
	Data []*ChargeStatus `json:"data"`
}

//ChargeStatus return the status we transfer money to that address.
type ChargeStatus struct {
	Code int    `json:"code"`
	Addr string `json:"chain_address"`
}

func newChargeStatus(err error, addr string) *ChargeStatus {
	code := checkChargeErrCode(err)
	return &ChargeStatus{
		Code: code,
		Addr: addr,
	}
}

//ChargeStatusResult return the result we post to url
type ChargeStatusResult struct {
	Code int `json:"code"`
	Msg  int `json:"msg"`
}

type chargeEvent struct {
	url    string
	status []*ChargeStatus
}

type ChargeHandler struct {
	chain Chain

	//http request event
	chargeCh chan *ChargeRequest
	resultCh chan *chargeEvent

	//pending task status
	pendingTask    int
	pendingTaskMux sync.Mutex

	quit chan struct{}
}

func NewChargeHandler(chain Chain) *ChargeHandler {
	ch := &ChargeHandler{
		chain:    chain,
		chargeCh: make(chan *ChargeRequest),
		resultCh: make(chan *chargeEvent),
		quit:     make(chan struct{}),
	}
	go ch.loop()
	return ch
}

//String show the main function of chain hanlder
func (ch *ChargeHandler) String() string {
	return "charge amount"
}

//ServeHTTP check charge request,then post charge event
func (ch *ChargeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method == "POST" {

		w.Header().Add("Content-Type", "application/json;charset=utf-8")

		b, err := ioutil.ReadAll(r.Body)
		if err != nil {
			log.Warn("ChargeHandler", "err", err)
			w.Write(encRespon(newChargeResponse(ErrHTTPRequest)))
			return
		}
		defer r.Body.Close()

		//parse request
		req := new(ChargeRequest)
		if err = json.Unmarshal(b, req); err != nil {
			log.Warn("ChargeHandler", "err", err)
			w.Write(encRespon(newChargeResponse(ErrHTTPRequest)))
			return
		}

		//check callback url
		if req.URL == "" {
			log.Warn("ChargeHandler", "err", "empty callback")
			w.Write(encRespon(newChargeResponse(ErrHTTPRequest)))
			return
		}

		//check chain data
		for _, dat := range req.Data {
			//invalid address
			if dat.Addr == "" {
				log.Warn("ChargeHandler", "err", "empty account address")
				w.Write(encRespon(newChargeResponse(ErrHTTPRequest)))
				return
			}
			//invalid amount
			if _, err := strconv.ParseFloat(dat.Amount, 64); err != nil {
				log.Warn("ChargeHandler", "err", "empty chaindata")
				w.Write(encRespon(newChargeResponse(ErrHTTPRequest)))
				return
			}
		}

		//post upload request event
		ch.PostChargeReq(req)
		w.Write(encRespon(newChargeResponse(nil)))

	} else {
		log.Warn("ChargeHandler", "unknown request", r.Method)
		w.Write(encRespon(newChargeResponse(ErrHTTPRequest)))
	}
}

//PostChargeReq post pending charge request event.
func (ch *ChargeHandler) PostChargeReq(req *ChargeRequest) error {
	select {
	case ch.chargeCh <- req:
	case <-ch.quit:
	}
	return nil
}

//PostChargeRes post done charge request event.
func (ch *ChargeHandler) PostChargeRes(url string, status []*ChargeStatus) error {
	select {
	case ch.resultCh <- &chargeEvent{url: url, status: status}:
	case <-ch.quit:
	}
	return nil
}

//Close stop handler
func (ch *ChargeHandler) Close() {
	close(ch.quit)
}

///loop wait event
func (ch *ChargeHandler) loop() {
	for {
		select {
		case req := <-ch.chargeCh:
			go ch.handleCharge(req)

		case event := <-ch.resultCh:
			res := &ChargeStatusResponse{Data: event.status}
			//post until succeed or arrive 50 time.
			go CallbackResponse(event.url, res)

		case <-ch.quit:
			return
		}
	}
}

//handleCharge send upload data to picoChain. After blocked,it will post done event
func (ch *ChargeHandler) handleCharge(req *ChargeRequest) {
	//upload data to the picoChain
	status := ch.charge(req)
	//post upload result,update pendig task
	ch.PostChargeRes(req.URL, status)
}

func (ch *ChargeHandler) charge(req *ChargeRequest) []*ChargeStatus {
	//result status
	result := make(chan *ChargeStatus, len(req.Data))
	datas := make([]*ChargeStatus, 0)

	//recording pending task
	ch.addPendingTask(len(req.Data))

	//charge money
	timeout := len(req.Data)/100 + 1
	for _, dat := range req.Data {
		//each request data upload blockchain seperately.
		go func(data *ChargeRequestData) {

			funds, err := strconv.ParseFloat(data.Amount, 64)
			if err != nil {
				log.Warn("charge error", "toAddr", data.Addr, "amount", data.Amount)
				result <- newChargeStatus(ErrHTTPRequest, data.Addr)
				return
			}

			//charge money and wait result
			err = ch.chain.Charge(data.Addr, funds, timeout)
			//response data
			result <- newChargeStatus(err, data.Addr)

		}(dat)
	}

	//wait and stat result
	for i := 0; i < len(req.Data); i++ {
		data := <-result
		datas = append(datas, data)
	}
	//all requst finish uploading, delete his pending task in this time
	ch.delPendingTask(len(req.Data))

	return datas
}

func (ch *ChargeHandler) addPendingTask(times int) {
	ch.pendingTaskMux.Lock()
	defer ch.pendingTaskMux.Unlock()

	ch.pendingTask = ch.pendingTask + times
}

func (ch *ChargeHandler) delPendingTask(times int) {
	ch.pendingTaskMux.Lock()
	defer ch.pendingTaskMux.Unlock()

	ch.pendingTask = ch.pendingTask - times

	//when user stop uploading data, clear his historical data
	if ch.pendingTask <= 0 {
		ch.chain.ClearChargeNonce()
		ch.pendingTask = 0
	}

}
